Unit BDojoSessionUnit;

{$mode objfpc}{$H+}

Interface

Uses
  Classes, SysUtils, strutils, fgl, HTTPDefs, BCustomSessionUnit, BRequestUnit,
    BDojoListenerUnit, BDojoHandlerUnit;

Type BProcedureClass = Procedure Of Object;
Type BFunctionClass = Procedure(Var aResponse: String) Of Object;

Type BDojoObjectClass = Class;
Type BDojoSessionClass = Class;

Type BDojoObjectsList = Specialize TFPGList<BDojoObjectClass>;

Type

{ BDojoObjectClass }

BDojoObjectClass = Class
  Private
    bAssociated: Boolean;
    bJSName: String;
    bJSType: String;
    bConfig: TStringList;
    bListeners: TStringList;
    bSession: BDojoSessionClass;
    bEarlyList: BDojoObjectsList;
    bLateList: BDojoObjectsList;
    bThis: BDojoObjectClass;

    Function GetEarlyInitialization: String;  Inline;
    Function GetLateInitialization: String; Inline;
    Function GetConfig: String;
  Protected
    bSrcNodeRef: String;

    Function BuildDynamicElement: String;

    Procedure SetConfig(Const aField: String; Const aValue: String;
      Const aCommas: Boolean = TRUE; Const aJSObject: Boolean = FALSE);
    Procedure SetConfig(Const aField: String; Const aValue: Integer);
    Procedure SetConfig(Const aField: String; Const aValue: Double);
    Procedure SetConfig(Const aField: String; Const aValue: TDateTime;
      Const aFormat: String); Inline;
    Procedure SetConfig(Const aField: String; Const aValue: Boolean);
    Procedure SetConfig(Const aField: String; Const aValue: BDojoObjectClass);

    Procedure AddAjax(Const aAjax: String; Const aFull: Boolean = TRUE);
    Procedure AddAjax(Const aAjaxMask: String;
      Const aParams: Array Of Const; Const aFull: Boolean = TRUE);

    Function RequestParam(Const aParam: String; Const aValue: String;
      Const aFull: Boolean = TRUE): String; Inline;
    Function RequestParam(Const aParam: String; Const aValueMask: String;
      Const aParams: Array Of Const;
      Const aFull: Boolean = TRUE): String; Inline;

    Procedure BeforeInitialization; Virtual;

    Function GetSession: BDojoSessionClass; Inline;


  Public
    //** Alias to Self
    Property This: BDojoObjectClass Read bThis;
    //** Flag indecating that this Object was associated already
    Property Associated: Boolean Read bAssociated;
    //** Type of Dojo Object
    Property JSType: String Read bJSType;
    //** Variable name of Dojo Object
    Property JSName: String Read bJSName;
    //** Contain config of Dojo Object
    Property Config: TStringList Read bConfig;
    //** Session-owner of Dojo Object
    Property Session: BDojoSessionClass Read bSession;

    //** Add another Dojo Object to list of associated to this. On
    //** JSInitialization will be added Initalization of all associated objects
    Procedure Associate(Const aObject: BDojoObjectClass);

    //** Run specified JSCode when event occurs
    Procedure Listen(Const aEvent: String; Const aMethod: String);
    //** Shortcut for Format with Listen
    Procedure Listen(Const aEvent: String; Const aMethod: String;
      Const aParams: Array Of Const);
    //** Call Ajax method when event occurs
    Procedure Listen(Const aEvent: String; Const aMethod: BProcedureClass;
      Const aJSParams: String = '');
    //** Shortcut for Format with Listen
    Procedure Listen(Const aEvent: String; Const aMethod: BProcedureClass;
      Const aParams: Array Of String; Const aJSParams: String = '');

    //** Format data of current request as String
    Function ByString(Const aParam: String): String; Inline;
    //** Format data of current request as Integer
    Function ByInteger(Const aParam: String): Integer; Inline;
    //** Format data of current request as Double
    Function ByDouble(Const aParam: String): Double; Inline;
    //** Format data of current request as TDateTime
    Function ByDateTime(Const aParam: String): TDateTime; Inline;
    //** Format data of current request as Boolean
    Function ByBoolean(Const aParam: String): Boolean; Inline;

    Function JSInitialization: String; Virtual;

    Constructor Build(Const aSession: BDojoSessionClass;
      Const aJSType: String = ''); Virtual;
    Destructor Burn; Virtual;
End;

Type

{ BDojoSessionClass }

BDojoSessionClass = Class(BCustomSessionClass)
  Private
    bDojoPath: String;
    bIndex: Integer;
    bObjects: BDojoObjectsList;
    bRequires: TStringList;
    bDynamicCSS: TStringList;
    bDynamicElements: TStringList;
    bListeners: BDojoListenersList;
    bHandlers: BDojoHandlersList;
    bTitle: String;
    bTheme: String;
    bSuspendedCode: String;
    bHandlerIndex: Integer;
    Function GetDynamicCSS: String;
    Function GetDynamicElements: String;
    Function GetJSName: String;
    Function GetRequies: String;
    Procedure ProcessListeners(Const aRequest: BRequestClass);
    Procedure RegisterObject(Const aObject: BDojoObjectClass);
  Protected
    bRequest: BRequestClass;
    bRoot: BDojoObjectClass;
    Function PrepareResponse(Const aRequires, aCSS, aElements, aJSCode: String;
      Const aFull: Boolean): String;
    Procedure Release; Override;
    //** Non-ajax processing. This method must be overriden
    Function ProcessRootRequest: BDojoObjectClass; Virtual; Abstract;
  Public
    //** HTML title of web page
    Property Title: String Read bTitle Write bTitle;
    //** DojoToolkit theme name. Default acceptable values is "nihilo", "claro",
    //** "tundra"
    Property Theme: String Read bTheme Write bTheme;
    //** Path to DojoToolkit framework. When compiled with directive
    //** -dGoogleCDN or -dYandexCDN it get default values for Google or Yandex
    //** servers
    Property DojoPath: String Read bDojoPath Write bDojoPath;

    //** Add AMD require script
    Procedure AddRequire(Const aRequire: String);
    //** Add CSS to dynamic load
    Procedure AddDynamicCSS(Const aDynamicCSS: String;
      Const aDojoToolkit: Boolean = FALSE);
    //** Add CSS to dynamic load with format shortcut
    Procedure AddDynamicCSS(Const aDynamicCSSMask: String;
      Const aParams: Array Of Const; Const aDojoToolkit: Boolean = FALSE);
    //** Add DOM element
    Procedure AddDynamicElement(Const aDynamicElement: String);
    //** Generated list of AMD requests for processing current request
    Property Requies: String Read GetRequies;
    //** Generated list of CSS requests for processing current request
    Property DynamicCSS: String Read GetDynamicCSS;
    //** Generated list of DOM changes for processing current request
    Property DynamicElements: String Read GetDynamicElements;

    //** Register listener for process from browser
    Function RegisterListener(Const aJSName, aEvent: String;
      Const aMethod: BProcedureClass): Integer;
    //** Register handler for process from browser for return data
    Function RegisterHandler(Const aPath: String;
      Const aMethod: BFunctionClass): String;

    //** Format data of current request as String
    Function ByString(Const aParam: String): String;
    //** Format data of current request as Integer
    Function ByInteger(Const aParam: String): Integer;
    //** Format data of current request as Double
    Function ByDouble(Const aParam: String): Double;
    //** Format data of current request as TDateTime
    Function ByDateTime(Const aParam: String): TDateTime;
    //** Format data of current request as Boolean
    Function ByBoolean(Const aParam: String): Boolean;

    //** Main method for processing of request
    Procedure ProcessRequest(Const aRequest: BRequestClass); override;

    Constructor Build(Const aID: Integer = -1); override;
End;

Implementation

Uses
  BDojoTemplateUnit;

{ BDojoObjectClass }

Function BDojoObjectClass.GetEarlyInitialization: String;
Var
  i: Integer;
Begin
  Result := '';
  If Assigned(bEarlyList) Then
    For i := 0 To bEarlyList.Count - 1 Do
      Result += bEarlyList[i].JSInitialization;
End;

Function BDojoObjectClass.GetLateInitialization: String;
Var
  i: Integer;
Begin
  Result := '';
  If Assigned(bLateList) Then
    For i := 0 To bLateList.Count - 1 Do
      Result += bLateList[i].JSInitialization;
End;

Function BDojoObjectClass.GetConfig: String;
Var
  i: Integer;
Begin
  Result := '';
  For i := 0 To bConfig.Count - 1 Do
    Result += ',' + bConfig[i];
  Delete(Result, 1, 1);
End;

Function BDojoObjectClass.BuildDynamicElement: String;
Begin
  Result := JSName + 'El';
  Session.AddDynamicElement(Result);
End;

Procedure BDojoObjectClass.SetConfig(Const aField: String;
  Const aValue: String; Const aCommas: Boolean; Const aJSObject: Boolean);
Begin
  If aJSObject Then
    Config.Values[aField] := '{' + aValue + '}'
  Else
    If aCommas Then Config.Values[aField] := '"' + aValue + '"'
    Else Config.Values[aField] := aValue;
End;

Procedure BDojoObjectClass.SetConfig(Const aField: String;
  Const aValue: Integer);
Begin
  SetConfig(aField, IntToStr(aValue), FALSE);
End;

Procedure BDojoObjectClass.SetConfig(Const aField: String;
  Const aValue: Double);
Begin
  //TODO: make clear
  SetConfig(aField, Format('%f', [aValue]));
End;

Procedure BDojoObjectClass.SetConfig(Const aField: String;
  Const aValue: TDateTime; Const aFormat: String);
Begin
  SetConfig(aField, FormatDateTime(aFormat, aValue));
End;

Procedure BDojoObjectClass.SetConfig(Const aField: String;
  Const aValue: Boolean);
Begin
  SetConfig(aField, BoolToStr(aValue, 'true', 'false'), FALSE);
End;

Procedure BDojoObjectClass.SetConfig(Const aField: String;
  Const aValue: BDojoObjectClass);
Begin
  SetConfig(aField, aValue.JSName, FALSE);
End;

Procedure BDojoObjectClass.AddAjax(Const aAjax: String; Const aFull: Boolean);
Begin
  If aFull Then Session.bSuspendedCode += JSName + '.' + aAjax
  Else Session.bSuspendedCode += aAjax
End;

Procedure BDojoObjectClass.AddAjax(Const aAjaxMask: String;
  Const aParams: Array Of Const; Const aFull: Boolean);
Begin
  AddAjax(Format(aAjaxMask, aParams), aFull);
End;

Function BDojoObjectClass.RequestParam(Const aParam: String;
  Const aValue: String; Const aFull: Boolean): String;
Begin
  Result := aParam + ':';
  If aFull Then Result += JSName + '.';
  Result += aValue;
End;

Function BDojoObjectClass.RequestParam(Const aParam: String;
  Const aValueMask: String; Const aParams: Array Of Const; Const aFull: Boolean
  ): String;
Begin
  Result := RequestParam(aParam, Format(aValueMask, aParams), aFull);
End;

Procedure BDojoObjectClass.BeforeInitialization;
Begin

End;

Function BDojoObjectClass.GetSession: BDojoSessionClass;
Begin
  Result := Session;
End;

Procedure BDojoObjectClass.Associate(Const aObject: BDojoObjectClass);
Begin
  If aObject.Associated Then Exit;
  If Not(Assigned(bEarlyList)) Then bEarlyList := BDojoObjectsList.Create;
  aObject.bAssociated := TRUE;
  bEarlyList.Add(aObject);
End;

Procedure BDojoObjectClass.Listen(Const aEvent: String; Const aMethod: String);
Begin
  AddAjax('on("%s", function(){%s});', [aEvent, aMethod]);
End;

Procedure BDojoObjectClass.Listen(Const aEvent: String; Const aMethod: String;
  Const aParams: Array Of Const);
Begin
End;

Procedure BDojoObjectClass.Listen(Const aEvent: String;
  Const aMethod: BProcedureClass; Const aJSParams: String);
Begin
  Listen(aEvent, aMethod, [], aJSParams);
End;

Procedure BDojoObjectClass.Listen(Const aEvent: String;
  Const aMethod: BProcedureClass; Const aParams: Array Of String;
  Const aJSParams: String);
Var
  i: Integer;
  aToken: Integer;
  aParamString: String = '';
Begin
  aToken := Session.RegisterListener(Self.JSName, aEvent, aMethod);
  For i := Low(aParams) To High(aParams) Do
    aParamString += aParams[i] + ',';

  bListeners.Add(Format(
    '%0:s.on("%1:s",function(){' +
      'dojo.xhrPost({' +
        'url: location.href + "/%1:s",' +
        'content: {' +
          '%2:s' +
          'ajax:1,' +
          'obj:"%0:s",' +
          'event:"%1:s",' +
          'token:%3:d' +
        '},'+
        'load: function(response){' +
          'eval(response);'+
        '},' +
        'error: function(){' +
        '}' +
      '})' +
    '});', [JSName, aEvent, aParamString, aToken]));
End;

Function BDojoObjectClass.ByString(Const aParam: String): String;
Begin
  Result := Session.ByString(aParam);
End;

Function BDojoObjectClass.ByInteger(Const aParam: String): Integer;
Begin
  Result := Session.ByInteger(aParam);
End;

Function BDojoObjectClass.ByDouble(Const aParam: String): Double;
Begin
 Result := Session.ByDouble(aParam);
End;

Function BDojoObjectClass.ByDateTime(Const aParam: String): TDateTime;
Begin
  Result := Session.ByDateTime(aParam);
End;

Function BDojoObjectClass.ByBoolean(Const aParam: String): Boolean;
Begin
  Result := Session.ByBoolean(aParam);
End;

Function BDojoObjectClass.JSInitialization: String;
Var
  i: Integer;
Begin
  BeforeInitialization;
  Result := GetEarlyInitialization;
  Result += JSName + ' = new ' + JSType + '({' + GetConfig + '}' +
    IfThen(Not(bSrcNodeRef = ''), ',' + bSrcNodeRef) + ');';
  Result += GetLateInitialization;

  For i := 0 To bListeners.Count - 1 Do
    Result += bListeners[i];
End;

Constructor BDojoObjectClass.Build(Const aSession: BDojoSessionClass;
  Const aJSType: String);
Begin
  bThis := Self;
  bSession := aSession;
  bSession.bObjects.Add(Self);
  bJSName := bSession.GetJSName;
  bJSType := aJSType;
  bSession.AddRequire(aJSType);
  bConfig := TStringList.Create;
  bConfig.NameValueSeparator := ':';
  bListeners := TStringList.Create;
End;

Destructor BDojoObjectClass.Burn;
Begin

End;

{ BDojoSessionClass }

Function BDojoSessionClass.GetJSName: String;
Begin
  Inc(bIndex);
  Result := 'O' + IntToStr(bIndex);
End;

Function BDojoSessionClass.GetDynamicCSS: String;
Var
  i: Integer;
Begin
  For i := 0 To bDynamicCSS.Count - 1 Do
    Result += ',"' + bDynamicCSS[i] + '"';
  Delete(Result, 1, 1);
end;

Function BDojoSessionClass.GetDynamicElements: String;
Var
  i: Integer;
Begin
  For i := 0 To bDynamicElements.Count - 1 Do
    Result += {'var ' + }bDynamicElements[i] + '=document.createElement("div");' +
      bDynamicElements[i] + '.id="' +bDynamicElements[i] + '";' +
      'dojo.body().appendChild(' + bDynamicElements[i] + ');';
End;

Function BDojoSessionClass.GetRequies: String;
Var
  i: Integer;
Begin
  Result := '';
  For i := 0 To bRequires.Count - 1 Do
    Result += ',"' + StringReplace(bRequires[i], '.','/', [rfReplaceAll]) + '"';
  Delete(Result, 1, 1);
  Result := 'require([' + Result + ']);';
end;

Procedure BDojoSessionClass.ProcessListeners(Const aRequest: BRequestClass);
Var
  i: Integer;
Begin
  For i := 0 To bListeners.Count - 1 Do
    If bListeners[i].Execute(aRequest.ByString('obj'),
      aRequest.ByString('event'), aRequest.ByInteger('token')) Then Break;

  aRequest.Response := bSuspendedCode;
  bSuspendedCode := '';
End;

Function BDojoSessionClass.PrepareResponse(Const aRequires, aCSS, aElements,
  aJSCode: String; Const aFull: Boolean): String;
Begin
  If aFull Then
    Result := Format(DOJO_TEMPLATE,
      ['%', aRequires, aCSS, aElements, aJSCode, Title, Theme, DojoPath])
  Else
    Result := aJSCode;
End;

Procedure BDojoSessionClass.RegisterObject(Const aObject: BDojoObjectClass);
Begin
  bObjects.Add(aObject);
End;

Procedure BDojoSessionClass.Release;
Var
  i: Integer;
Begin
  For i := 0 To bObjects.Count - 1 DO
    bObjects[i].Burn;
  bObjects.Free;
End;

Procedure BDojoSessionClass.AddRequire(Const aRequire: String);
Begin
  If bRequires.IndexOf(aRequire) = -1 Then
    bRequires.Add(aRequire);
End;

Procedure BDojoSessionClass.AddDynamicCSS(Const aDynamicCSS: String;
  Const aDojoToolkit: Boolean);
Begin
  If bDynamicCSS.IndexOf(aDynamicCSS) = -1 THen
    bDynamicCSS.Add(IfThen(aDojoToolkit, DojoPath, '') + aDynamicCSS);
End;

Procedure BDojoSessionClass.AddDynamicCSS(Const aDynamicCSSMask: String;
  Const aParams: Array Of Const; Const aDojoToolkit: Boolean);
Begin
  AddDynamicCSS(Format(aDynamicCSSMask, aParams), aDojoToolkit);
End;

Procedure BDojoSessionClass.AddDynamicElement(Const aDynamicElement: String);
Begin
  If bDynamicElements.IndexOf(aDynamicElement) = -1 THen
    bDynamicElements.Add(aDynamicElement);
End;

Function BDojoSessionClass.RegisterListener(Const aJSName, aEvent: String;
  Const aMethod: BProcedureClass): Integer;
Var
  aListener: BDojoListenerClass;
Begin
  aListener := BDojoListenerClass.Build(aJSName, aEvent, aMethod);
  bListeners.Add(aListener);
  Result := aListener.Token;
End;

Function BDojoSessionClass.RegisterHandler(Const aPath: String;
  Const aMethod: BFunctionClass): String;
Var
  aHandlerLabel: String = '';
Begin
  bHandlerIndex += 1;
  If aPath = '' Then aHandlerLabel := 'request' + IntToStr(bHandlerIndex)
  Else aHandlerLabel := aPath;

  bHandlers.Add(BDojoHandlerClass.Build(aHandlerLabel, aMethod));
  Result := aHandlerLabel;
End;

Function BDojoSessionClass.ByString(Const aParam: String): String;
Begin
  Result := HTTPDecode(bRequest.PostQueries.Values[aParam]);
End;

Function BDojoSessionClass.ByInteger(Const aParam: String): Integer;
Begin
  Result := StrToIntDef(bRequest.PostQueries.Values[aParam], -1);
End;

Function BDojoSessionClass.ByDouble(Const aParam: String): Double;
Begin
  Result := StrToFloatDef(bRequest.PostQueries.Values[aParam], -1);
End;

Function BDojoSessionClass.ByDateTime(Const aParam: String): TDateTime;
Begin
  Result := StrToDateTimeDef(bRequest.PostQueries.Values[aParam], 0);
End;

Function BDojoSessionClass.ByBoolean(Const aParam: String): Boolean;
Begin
  Result := StrToBoolDef(bRequest.PostQueries.Values[aParam], FALSE);
End;

Procedure BDojoSessionClass.ProcessRequest(Const aRequest: BRequestClass);
Var
  i: Integer;
Begin
  bRequest := aRequest;
  If aRequest.ByInteger('ajax') = 1 Then
    Begin
      ProcessListeners(aRequest);
      Exit;
    End;
  For i := 0 TO bHandlers.Count - 1 Do
    If bHandlers[i].Execute(aRequest) Then Exit;

  If bRoot = nil Then bRoot := ProcessRootRequest;
  If Not(bRoot = nil) Then
    aRequest.Response := PrepareResponse(Requies, DynamicCSS, DynamicElements,
      bRoot.JSInitialization + bSuspendedCode, TRUE)
  Else
    aRequest.Response := 'Создайте корневой объект';
  bSuspendedCode := '';
End;

Constructor BDojoSessionClass.Build(Const aID: Integer);
Begin
  Theme := 'nihilo';
  Title := 'DojoPas';
  bRequires := TStringList.Create;
  bDynamicCSS := TStringList.Create;
  bObjects := BDojoObjectsList.Create;
  bDynamicElements := TStringList.Create;
  bListeners := BDojoListenersList.Create;
  bHandlers := BDojoHandlersList.Create;

  {$IFDEF GoogleCDN}
  bDojoPath := 'http://ajax.googleapis.com/ajax/libs/dojo/1.7.2';
  {$ENDIF}
  {$IFDEF YandexCDN}
  bDojoPath := 'http://yandex.st/dojo/1.7.2';
  {$ENDIF}
  Inherited Build(aID);
End;

End.

